/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package Communication;

import Common.*;
import Response.*;
import Request.*;
import Request.GeneralRequest.GRType;
import java.net.*;
import java.io.*;
import java.util.*;


//For socket channel
import java.nio.channels.*;

import javax.sound.midi.SysexMessage;


/**
 *
 * @author lcy
 */
public class ClientTransceiver extends Communicator{

    private Socket _sock;
    private InputStream _in_s;
    private OutputStream _out_s;
    private ObjectOutput _out_channel;
    private ObjectInput _in_channel;

    /*Note: socketchannel and selector are only for future development*/
    private SocketChannel _sChannel;
    private Selector selector;

    private InfoLogger _debugger;

    private UdpWaitSockThread _wait_sock;

    private P2PSockThread _p2p_sock;

    private MailThread mailServer;
    private ArrayList<PeerThread> peerThreadsList;
    private RequestHandler rHandler;

    public ClientTransceiver(){
        _sChannel = null;
        selector = null;
        InitCommComponents();
    }

    public ClientTransceiver(String ip, int port, RequestHandler handler) throws UnknownHostException,IOException{
        try{
            InetSocketAddress remote_addr = new InetSocketAddress(ip,port);
            
            selector = Selector.open();

            _sChannel = SocketChannel.open();
            _sChannel.configureBlocking(false);
            
            _sChannel.connect(remote_addr);
            _sChannel.register(selector, _sChannel.validOps());

            ///////////////////////////////////////////////////////
            _sock = new Socket(ip,port);
            _out_s = _sock.getOutputStream();
            _in_s = _sock.getInputStream();
            _out_channel = new ObjectOutputStream(_out_s);
            _in_channel = new ObjectInputStream(_in_s);
            rHandler = handler;
        }catch(IOException e2){
            throw e2;
        }

        InitCommComponents();
    }

    private void InitCommComponents(){

        _wait_sock = new UdpWaitSockThread();

        _p2p_sock = new P2PSockThread();

        try
        {
            mailServer = new MailThread(CONSTANTS.MAIL_THREAD_PORT);
            
        }
        catch(Exception e)
        {
            System.out.println(e.getMessage());
        }

        peerThreadsList = new ArrayList<PeerThread>();
        
        _debugger = new InfoLogger();
    }

    public void StartTransceiver(){
        
    }

    public void StartMailSever(){
        mailServer.start();
    }

    /**
     * Function name: SendLoginRequest
     * Summary: Send out a Login request
     * Input: login request: the information for the request
     * Output: none
     * Side Effect: none
     * Pre condition: 1). Login_request must not be null
     *                2). The tcp socket must be opened.
     */

    public void SendLoginRequest(LoginRequest login_request)throws NullPointerException{

	if(login_request == null){
	    throw new NullPointerException("SendLoginRequest: request is null.");
	}
        try{
            login_request.SetLocalInetAddr(_sock.getLocalAddress());
            login_request.SetLocalPort(_sock.getLocalPort());

            _debugger.LogInfo("ID " + login_request.GetRequestContent().GetUserID()
                    + "passwd: " + login_request.GetRequestContent().GetPasswdRaw());
            _out_channel.writeObject(login_request);
            _out_channel.flush();

        }catch(IOException e){
            e.printStackTrace();
            _debugger.LogInfo(e.getMessage());
        }
    }

    /**
     * Function name:
     * Summary: Send out a registration request
     * Input: The register request
     * Output: none
     * Side Effect:
     * Pre condition: new_user_request should not be null
     * the tcp socket must be opened.
     */
    public void SendRegisterRequest(RegisterRequest new_user_request) throws NullPointerException{

        if(new_user_request == null){
            throw new NullPointerException("Register: new_user_request is null");
        }

        try{
            new_user_request.SetLocalInetAddr(_sock.getLocalAddress());
            new_user_request.SetLocalPort(_sock.getLocalPort());

            _debugger.LogInfo("UserID " +new_user_request.GetRequestContent().GetUserID() +
                    " passwd: " + new_user_request.GetRequestContent().GetPasswdRaw());
            _out_channel.writeObject(new_user_request);
            _out_channel.flush();

        }catch(IOException e){
            e.printStackTrace();
            _debugger.LogInfo(e.getMessage());
        }
    }

    public void SendUserListReqeust(UserListRequest request){
        request.SetLocalInetAddr(_sock.getLocalAddress());
        request.SetLocalPort(_sock.getLocalPort());
        try{
            _out_channel.writeObject(request);
            _out_channel.flush();
        }catch(IOException e){
            e.printStackTrace();
            _debugger.LogInfo(e.getMessage());
        }
    }

    public synchronized NetDataResponse RxResponse(RequestType r_type){
    
        try{
            
            switch(r_type){

                case LOGIN:
                case REGISTRATION:
                     NetDataLRResp ret1 = (NetDataLRResp) _in_channel.readObject();
                     return ret1;
                case USERLIST:
                    NetDataUserListResp ret2 =
                            (NetDataUserListResp) _in_channel.readObject();
                    return ret2;
            }
            
            
            
        }catch(IOException e){
            e.printStackTrace();
            _debugger.LogInfo(e.getMessage());
            return null;
        }catch(ClassNotFoundException e2){
            e2.printStackTrace();
            _debugger.LogInfo(e2.getMessage());
            return null;
        }

        return null;
    }

    public boolean NonBLockRxResponse(NetDataResponse result){

        try {
        // Wait for an event
        selector.select();
        } catch (IOException e) {
            // Handle error with selector
            return false;
        }

        // Get list of selection keys with pending events
        Iterator it = selector.selectedKeys().iterator();

        while (it.hasNext()) {
            // Get the selection key
            SelectionKey selKey = (SelectionKey)it.next();
            // Remove it from the list to indicate that it is being processed
            it.remove();
            try {
                processSelectionKey(selKey);
            } catch (IOException e) {
                // Handle error with channel and unregister
                selKey.cancel();
                return false;
            }
        }

        return true;
    }

    // taken from http://www.exampledepot.com/egs/java.nio/NbClient.html
    private boolean processSelectionKey(SelectionKey selKey) throws IOException {
        // Since the ready operations are cumulative,
        // need to check readiness for each operation
        if (selKey.isValid() && selKey.isConnectable()) {
            // Get channel with connection request
            SocketChannel sChannel = (SocketChannel)selKey.channel();

            //Check if our socket is connected;
            boolean success = sChannel.finishConnect();

            if (!success) {
                // An error occurred; handle it
                _debugger.LogInfo("Client channel: finish connect fails");
                // Unregister the channel with this selector
                selKey.cancel();
                return false;
            }
            
        }
        if (selKey.isValid() && selKey.isReadable()) {
            // Get channel with bytes to read
            SocketChannel sChannel = (SocketChannel)selKey.channel();

            // See Reading from a SocketChannel
        }

        if (selKey.isValid() && selKey.isWritable()) {
            // Get channel that's ready for more bytes
            //SocketChannel sChannel = (SocketChannel)selKey.channel();

            // See Writing to a SocketChannel
        }

        return true;
    }

    /*****************************************************
     * Function:close()
     * Summary: close all I/O streams and sockets
     * Input: none
     * Returns: none
     * Side effects:
     * Pre-Conditions: none
    ******************************************************/
    public void close(){
        try{
            _out_channel.close();
            _in_channel.close();
            _in_s.close();
            _out_s.close();
            _sock.close();
        }catch(IOException ioe){
            _debugger.LogInfo("Client Transceiver: closing channel error");
        }catch(NullPointerException ex){
            //A null pointer at close is harmless.
            _debugger.LogInfo("Client Transceiver: one of IO channels is null "
                    + "when closing");
        }
    }

    public boolean OpenUDPWaitSocket(InetAddress ip, int port){
         return _wait_sock.OpenUDPWaitSocket(ip, port);
    }

    public void SetWaitUserName(String name){
        _wait_sock.SetRespUserName(name);
    }

    public void StartUDPWaitSock(){
        _wait_sock.start();

        
    }

    public void JoinUDPWaitSock(){
        try{
            _wait_sock.join();
        }catch(InterruptedException ex){
            //Do nothing for now;
        }
    }

    public void StartP2PSock(){
        if(_p2p_sock == null){
            _debugger.LogInfo("_p2p_sock is null");
            return;
        }
        _p2p_sock.start();
    }

    public void JoinP2PSock(){
        try{
            _p2p_sock.join();
        }catch(InterruptedException ex){
            //Do nothing for now;
        }
    }
    
    /**
     * Send out the general request to one / more peers
     * @param request: the general request, it must contains the "IP" field
     */
    public void sendGeneralRequest(GeneralRequest request)throws InvalidClassException, IOException{
        
        String ips = request.getDestAddresses();

        if(ips == null){
            //i.e. send to all
        }else{
            
            String [] addrList = getAddrList(ips);

            //For each designated address,send the request
            //if possible;
            int idx;
            for(int i = 0; i < addrList.length; i++){
                if((idx = FindAddrInList(addrList[i])) != -1){
                    PeerThread pThread = peerThreadsList.get(idx);
                    try{
                        pThread.sendGenRequest(request);
                    }catch(InvalidClassException  ex1){
                        throw new InvalidClassException("bad request");
                    }catch(IOException ex2){
                        throw new IOException("CTrans: sendGeneralRequest fails");
                    }
                }
            }
        }
    }

    private String [] getAddrList(String addrList){
        return addrList.split(" ");
    }

    /**
     * Find the index of the peerThread that message to be sent to
     * @param ip the ip of the remote peer's address
     * @return the index of the peerThread, -1 if not found;
     */
    private int FindAddrInList(String ip){

        for(int i  = 0; i < peerThreadsList.size(); i ++){
            
            PeerThread nextThread = peerThreadsList.get(i);
            try{
                InetAddress addr = InetAddress.getByName(ip);
                if(addr == nextThread.getInetAddress()){
                    return i;
                }
            }catch(UnknownHostException e){
                //Do nothing
            }
        }
        return -1;
    }

    public void sendJoinRequest(GeneralRequest r){
        //Create a PeerThread
        try{
            String ip = (String)r.getFieldValue("ip");
            int port = (Integer)r.getFieldValue("port");
            Socket tempSock = new Socket(ip,port);
            PeerThread theThread = new PeerThread(tempSock, rHandler);
            peerThreadsList.add(theThread);
            theThread.sendGenRequest(r);
            theThread.start();

        }catch(UnknownHostException e)
        {
            System.out.println(e.getMessage());
        }
        catch(IOException e)
        {
            System.out.println(e.getMessage());
        }
        //Add the peerThread to list
    }

    public void sendInvitation(GeneralRequest r)
    {
        try
        {
            String ip = (String)r.getFieldValue("ip");
            int port = (Integer)(r.getFieldValue("port"));
            Socket tempSock = new Socket(ip, port);
            ObjectOutput tempOutChannel = new ObjectOutputStream(tempSock.getOutputStream());
            r.SetLocalInetAddr(tempSock.getLocalAddress());
            r.SetLocalPort(tempSock.getLocalPort());
            tempOutChannel.writeObject(r);
            tempSock.close();
        }
        catch(UnknownHostException e)
        {
            System.out.println(e.getMessage());
        }
        catch(IOException e)
        {
            System.out.println(e.getMessage());
        }
    }
    
}
